--- id: TASK-020 title: Spellcheck with undercurl underlines status: "\U0001F7E2 In progress" assignee: [] created_date: '2026-06-29 18:11' updated_date: '2026-06-30 00:21' labels: - feature - release-1 dependencies: [] priority: medium ordinal: 20000 --- ## Description Lightweight inline spellcheck: red wavy (undercurl) underline under misspelled words, with one-key suggest-and-replace. Catch obvious typos only — not an intense/morphological checker. Must stay zero-cgo so the Homebrew/go-install build keeps working, and must not bloat the binary much. RENDERING (undercurl) - Misspelled-word spans emit raw SGR: undercurl '\e[4:3m' + underline color '\e[58:2::R:G:Bm' (theme-driven red, e.g. Flexoki red 217,54,42), reset '\e[59;4:0m'. - lipgloss/termenv DON'T expose undercurl or underline-color — add a raw-escape span attribute in the editor scanner. Preserve the markup-visible invariant: span text still concatenates to the raw line; only the SGR wrapper changes. - Degrade gracefully: terminals without 4:3 show straight underline or ignore (Ghostty/kitty/WezTerm/foot/VTE support it). tmux needs terminal-features passthrough — document it. DICTIONARY (lightweight, pure-Go) - Embed a single common-English wordlist (SCOWL-derived, ~50-60k most-common words; gzip-embedded via embed.FS, decompressed into a hashset at startup). Target small added binary weight, not full coverage. - No cgo, no hunspell — keeps 'go build .' / brew formula clean. - Case-insensitive membership; treat possessives/simple plurals leniently if cheap. SUGGEST & REPLACE - Build a BK-tree from the same embedded dict at startup (edit-distance <=2 lookup; modest RAM, no extra binary weight; only queried on user trigger, never per render). - Trigger key (suggest Ctrl+; TBD) when the cursor is on/adjacent to a flagged word opens a small popup of the top ~5 suggestions ranked by edit distance (tie-break by word frequency/length). - Pick with arrows+Enter or a number key -> replace the misspelled word span in the buffer, mark dirty, clear its underline. Esc dismisses. - The same popup includes an 'Add to dictionary' entry (writes the word to dict.txt) so add-word and replace share one UI. WHAT TO SKIP (no false positives) - Code fences and inline code, URLs, wikilinks [[...]], markdown link targets, YAML frontmatter values, and (when on in a code file) anything non-prose. PERSONAL DICTIONARY (easy add) - Plain file ~/.config/glint/dict.txt, one word per line, user-editable by hand. - 'Add to dictionary' from the suggestion popup (or a direct add-word key) appends to dict.txt and clears the underline live. TOGGLE / DEFAULTS - On/off toggle (key TBD) for the session. - Default ON for .md/.markdown/.txt and unnamed buffers; default OFF for recognized code-file extensions (driven by the same extension map as TASK-018 syntax highlighting — share it). - Config key to override the default (e.g. spellcheck = auto|on|off). PERF - Check only visible-viewport words per render; cache word->ok results so typing doesn't re-check the whole doc each keystroke; invalidate a word's cache entry when added to the personal dict. Suggestion lookups run only on trigger. ## Acceptance Criteria - [ ] #1 Misspelled prose words show a red undercurl underline in Ghostty; unsupported terminals degrade to plain/no underline - [ ] #2 Dictionary is pure-Go embedded (no cgo); binary size increase is modest and 'go build .' + brew formula still work - [ ] #3 Code fences, inline code, URLs, wikilinks, link targets, and frontmatter values are never flagged - [ ] #4 Adding the word under the cursor appends to ~/.config/glint/dict.txt and removes the underline live; hand-editing dict.txt also works - [ ] #5 Spellcheck defaults ON for md/txt/unnamed buffers and OFF for code files, with a config override and a session toggle - [ ] #6 Triggering on a flagged word shows ~5 ranked suggestions; picking one replaces the word in place, marks the buffer dirty, and clears the underline